/* 
 * Copyright 2010 Ziya Guven Koylu, Ozge Koylu
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package com.koylu.caffein;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Map;
import java.util.concurrent.locks.Lock;

import com.koylu.caffein.cache.CacheManager;
import com.koylu.caffein.lock.LockManager;
import com.koylu.caffein.model.caffein.CaffeinConfig;
import com.koylu.caffein.model.clazz.Clazz;
import com.koylu.caffein.model.clazz.Relation;
import com.koylu.caffein.model.clazz.validation.ValidateFor;
import com.koylu.caffein.model.container.ClazzContainer;
import com.koylu.caffein.proc.Proc;
import com.koylu.caffein.query.Query;

public class Caffein {

	private CaffeinConfig caffeinConfig;
	private ObjectToQuery objectToQuery;
	private CacheManager cacheManager;
	private LockManager lockManager;

	public CaffeinConfig getCaffeinConfig() {
		return caffeinConfig;
	}

	public void setCaffeinConfig(CaffeinConfig caffeinConfig) {
		this.caffeinConfig = caffeinConfig;
		objectToQuery = caffeinConfig.getObjectToQuery();
		cacheManager = caffeinConfig.getCacheManager();
		lockManager = caffeinConfig.getLockManager();
	}
	
	public Clazz getClazz(String type) throws Exception {
		Clazz clazz = caffeinConfig.getClazz(type);
		if(clazz == null){
			throw new RuntimeException("clazz not found:"+type);
		}
		return clazz;
	}
	
	public Clazz getClazz(Class<?> clazz) throws Exception {
		Clazz clazzz = caffeinConfig.getClazz(clazz.getName());
		if(clazzz == null){
			throw new RuntimeException("clazz not found:"+clazz.getName());
		}
		return clazzz;
	}
	
	public Proc getProc(String type) throws Exception{
		Proc proc = caffeinConfig.getProc(type);
		if(proc == null){
			throw new RuntimeException("proc not found:"+type);
		}
		return proc;
	}
	
	public Proc getProc(Class<?> clazz) throws Exception{
		Proc proc = caffeinConfig.getProc(clazz.getName());
		if(proc == null){
			throw new RuntimeException("proc not found:"+clazz.getName());
		}
		return proc;
	}
	
	public Object createEmptyObjectForProc(String type) throws Exception{
		return getProc(type).toEmptyObject(caffeinConfig);
	}
	
	public Object createEmptyObject(String type) throws Exception{
		return getClazz(type).toEmptyObject(caffeinConfig);
	}
	
	public Query createQuery(String type, String alias) throws Exception {
		return caffeinConfig.createQuery(getClazz(type), alias);
	}
	
	public Query createQuery(Class<?> clazz, String alias) throws Exception {
		return caffeinConfig.createQuery(getClazz(clazz), alias);
	}
	
	public Query createQuery(Clazz clazz, String alias) throws Exception {
		return caffeinConfig.createQuery(clazz, alias);
	}
	
	public Transaction createTransaction() {
		return new Transaction(this);
	}

	public void validate(Object object, ValidateFor validateFor) throws Exception {
		getClazz(object.getClass().getName()).validate(caffeinConfig, object, validateFor);
	}
	
	public Object load(String type, Object id) throws Exception {
		return load(getClazz(type), id);
	}

	public Object load(Class<?> clazz, Object id) throws Exception {
		return load(getClazz(clazz.getName()), id);
	}
	
	public Object load(Clazz clazz, Object id) throws Exception {
		boolean clazzCacheable = clazz.isCacheable(caffeinConfig);
		String clazzName = clazz.getName();
		String key = clazz.getId().toUniqueId(caffeinConfig, id);
		Object object = null;
		if(clazzCacheable){
			object = cacheManager.get(clazzName, key);
			if(object != null){
				if(clazz.isReadonly()){
					return object;
				}
				return clazz.copy(caffeinConfig, object);
			}	
		}
		Connection connection = null;
		PreparedStatement preparedStatement = null;
		ResultSet resultSet = null;
		try {
			connection = caffeinConfig.getConnection();
			preparedStatement = objectToQuery.load(caffeinConfig, connection, clazz, id);
			resultSet = preparedStatement.executeQuery();
			if (resultSet.next()) {
				object = clazz.toObject(caffeinConfig, resultSet);
			}
			if(clazzCacheable){
				if(object != null){
					cacheManager.put(clazzName, key, object);
					if(clazz.isReadonly()){
						return object;
					}
					return clazz.copy(caffeinConfig, object);
				}
			}
			return object;
		} catch (Exception e) {
			throw e;
		} finally {
			if (resultSet != null) {
				try {
					resultSet.close();					
				} catch (Exception e) {}
			}
			if (preparedStatement != null) {
				try {
					preparedStatement.close();							
				} catch (Exception e) {}
			}
			if (connection != null) {
				try {
					connection.close();							
				} catch (Exception e) {}
			}
		}
	}
	
	public Object load(Clazz clazz, Connection connection, Object id) throws Exception {
		Object object = null;
		PreparedStatement preparedStatement = null;
		ResultSet resultSet = null;
		try {
			preparedStatement = objectToQuery.load(caffeinConfig, connection, clazz, id);
			resultSet = preparedStatement.executeQuery();
			if (resultSet.next()) {
				object = clazz.toObject(caffeinConfig, resultSet);
			}
		} catch (Exception e) {
			throw e;
		} finally {
			if (resultSet != null) {
				try {
					resultSet.close();					
				} catch (Exception e) {}
			}
			if (preparedStatement != null) {
				try {
					preparedStatement.close();							
				} catch (Exception e) {}
			}
		}
		return object;
	}

	public void save(Object object) throws Exception {
		Clazz clazz = getClazz(object.getClass().getName());
		Connection connection = null;
		try {
			connection = caffeinConfig.getConnection();
			connection.setAutoCommit(false);
			save(clazz, connection, object);
			connection.commit();
		} catch (Exception e) {
			if (connection != null) {
				connection.rollback();
			}
			throw e;
		} finally {
			if (connection != null) {
				try {
					connection.close();	
				} catch (Exception e) {}
			}
		}
	}

	public void save(Clazz clazz, Connection connection, Object object) throws Exception {
		if(clazz.isReadonly()){
			throw new RuntimeException("save is not permitted clazz is readonly");
		}
		clazz.validate(caffeinConfig, object, ValidateFor.SAVE);
		PreparedStatement preparedStatement = null;
		int result = 0;
		try {
			preparedStatement = objectToQuery.save(caffeinConfig, connection, clazz, object);
			result = preparedStatement.executeUpdate();
			if( result > 0 ){
				if(caffeinConfig.supportsGetGeneratedKeys()){
					clazz.getId().setGeneratedKeys(caffeinConfig, object, preparedStatement);					
				}
			}
		} catch (Exception e) {
			throw e;
		} finally {
			if (preparedStatement != null) {
				try {
					preparedStatement.close();					
				} catch (Exception e) {}
			}
		}
		if( result > 0 ){
			if (clazz.getRelationMap() != null) {
				for (Relation relation : clazz.getRelationMap().values()) {
					relation.save(caffeinConfig, connection, object);
				}
			}
		}
	}

	public boolean update(Object object) throws Exception {
		boolean result = false;
		Clazz clazz = getClazz(object.getClass().getName());
		Connection connection = null;
		try {
			connection = caffeinConfig.getConnection();
			connection.setAutoCommit(false);
			result = update(clazz, connection, object);
			connection.commit();
		} catch (Exception e) {
			if (connection != null) {
				connection.rollback();
			}
			throw e;
		} finally {
			if (connection != null) {
				try {
					connection.close();	
				} catch (Exception e) {}
			}
		}
		return result;
	}

	public boolean update(Clazz clazz, Connection connection, Object object) throws Exception {
		if(clazz.isReadonly()){
			throw new RuntimeException("update is not permitted clazz is readonly");
		}
		if( !(object instanceof ClazzContainer) ){
			throw new RuntimeException("invalid object load it from caffein");
		}
		ClazzContainer clazzContainerObject = (ClazzContainer)object;
		if (clazzContainerObject.isContainerUpdated()) {
			clazz.validate(caffeinConfig, clazzContainerObject, ValidateFor.UPDATE);
			PreparedStatement preparedStatement = null;
			Lock lock = lockManager.getLock(clazz.getName(), clazzContainerObject.getContainerUniqueId());
			lock.lock();
			try {
				if(clazz.getOptimisticLockProperty() != null){
					if(!clazz.getOptimisticLockProperty().checkObjectVersion(caffeinConfig, clazz, connection, clazzContainerObject)){
						return false;
					}
				}
				int result = 0;
				preparedStatement = objectToQuery.update(caffeinConfig, connection, clazz, object);
				result = preparedStatement.executeUpdate();
				if( result > 0 ){
					if (clazz.getRelationMap() != null) {
						for (Relation relation : clazz.getRelationMap().values()) {
							relation.update(caffeinConfig, connection, object);
						}
					}
					if(clazz.isCacheable(caffeinConfig)){
						cacheManager.remove(clazz.getName(), clazz.getId().toUniqueId(caffeinConfig, ((ClazzContainer) object).getContainerId()));
					}
					return true;
				}	
			} catch (Exception e) {
				throw e;
			}finally{
				if (preparedStatement != null) {
					try {
						preparedStatement.close();							
					} catch (Exception e) {}
				}
				lock.unlock();
			}
		}
		return false;
	}
	
	public boolean delete(String type, Object id) throws Exception {
		return delete(getClazz(type), id);
	}

	public boolean delete(Class<?> clazz, Object id) throws Exception {
		return delete(getClazz(clazz.getName()), id);
	}

	public boolean delete(Clazz clazz, Object id) throws Exception {
		boolean result = false;
		Connection connection = null;
		try {
			connection = caffeinConfig.getConnection();
			connection.setAutoCommit(false);
			result = delete(clazz, connection, id);
			connection.commit();
		} catch (Exception e) {
			if (connection != null) {
				connection.rollback();
			}
			throw e;
		} finally {
			if (connection != null) {
				try {
					connection.close();	
				} catch (Exception e) {}
			}
		}
		return result;
	}

	public boolean delete(Clazz clazz, Connection connection, Object id) throws Exception {
		if(clazz.isReadonly()){
			throw new RuntimeException("delete is not permitted clazz is readonly");
		}
		PreparedStatement preparedStatement = null;
		Lock lock = lockManager.getLock(clazz.getName(), clazz.getId().toUniqueId(caffeinConfig, id));
		lock.lock();
		try {
			Object object = load(clazz, connection, id);
			if (object != null) {
				clazz.validate(caffeinConfig, object, ValidateFor.DELETE);
				if (clazz.getRelationMap() != null) {
					for (Relation relation : clazz.getRelationMap().values()) {
						relation.delete(caffeinConfig, clazz, connection, object);
					}
				}
				preparedStatement = objectToQuery.delete(caffeinConfig, connection, clazz, id);
				if( preparedStatement.executeUpdate() > 0){
					if(clazz.isCacheable(caffeinConfig)){
						cacheManager.remove(clazz.getName(), clazz.getId().toUniqueId(caffeinConfig, id));
					}
					return true;
				}				
			}	
		} catch (Exception e) {
			throw e;
		}finally{
			if (preparedStatement != null) {
				try {
					preparedStatement.close();							
				} catch (Exception e) {}
			}
			lock.unlock();
		}
		return false;
	}
	
	public void callProc(Object object) throws Exception{
		callProc(getProc(object.getClass()), object);
	}
	
	public void callProc(Proc proc, Object object) throws Exception{
		proc.call(caffeinConfig, object);
	}
	
	public void callProc(Proc proc, Map<String, Object> map) throws Exception{
		proc.call(caffeinConfig, map);
	}
}